package com.android.openapp.services;

//import java.util.ArrayList;

import com.android.openapp.R;
import com.android.openapp.data.Place;
import com.android.openapp.ui.MainActivity;
import com.android.openapp.ui.SettingsFragment;
import com.socrata.api.HttpConnection;
import com.socrata.util.Variables;

import android.app.Activity;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.location.Geocoder;
import android.location.GpsStatus;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.GpsStatus.Listener;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;

/**
 * Servizio che sta dietro all'Activity
 * si occupa di lanciare i thread del download, recuperare la posizione tramite GPS,
 * recuperare le impostazioni, ...
 * @author Simone Panza
 * 
 *
 */

public class Controller extends Service {
	/**
	 * @author Allieri Tommaso
	 * Parametri connessione e istanza di connessione
	 */
	private static String domain="dati.lombardia.it";
	private static String user="m";
	private static String password="f";
	private static String apptoken="z2eM5pVNIh71ovXSX426RI2un";
	
	public static HttpConnection c=new HttpConnection(domain, user, password, apptoken);
	/**
	 * Fine parametri di connessione
	 */
	
	private Activity mActivity;
	private ControllerBinder mBinder;
	private LocationManager mLocationM;
	private ConnectivityManager mConnManager;
	
	static Location lastKnownPosition;
	
	// STRINGS
	public final static String MY_PREFS = "openAppPrefs";
	
	private int STATUS;
	
	public static final int STATUS_DISCONNECTED = 0;
	public static final int STATUS_CONNECTED = 1;
	public static final int STATUS_METADATA_READY = 2;
	
	private int METADATA_STATUS;
	private boolean firstAttemptFailed;
	
	private static final int STATUS_METADATA_ABSENT = 3;
	private static final int STATUS_METADATA_RETRIEVING = 4;
	private static final int STATUS_METADATA_DOWNLOADED = 5;
	
	private static final String ACTION_NETWORK_DISCONNECT = "ACTION_NETWORK_DISCONNECT";
	private static final String ACTION_NETWORK_CONNECT = "ACTION_NETWORK_CONNECT";
	private static final String ACTION_METADATA_DOWNLOADED = "ACTION_METADATA_DOWNLOADED";
	private static final String ACTION_METADATA_DOWNLOAD_ERROR = "ACTION_METADATA_DOWNLOAD_ERROR";
	
	//parametri location GPS/network
	private static long LOCATION_UPDATES_PERIOD = 60000L;
	private static final int DELTA_ACC_THRESHOLD_WIFI = 1;
	private static final int DELTA_ACC_THRESHOLD_MOBILE = 100;
	private static final long DELTA_TIME_THRESHOLD_WIFI = 1 * LOCATION_UPDATES_PERIOD;
	private static final long DELTA_TIME_THRESHOLD_MOBILE = 2 * LOCATION_UPDATES_PERIOD;
	
	private static final int TYPE_NETWORK_WIFI = 0;
	private static final int TYPE_NETWORK_MOBILE = 1;
	
	private int TYPE_NETWORK;
	
	@Override
	public void onCreate() {
		super.onCreate();
		mBinder = new ControllerBinder();
		
		mLocationM = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
		mLocationM.addGpsStatusListener(mGPSStatusListener);
		mLocationM.requestLocationUpdates(LocationManager.GPS_PROVIDER, LOCATION_UPDATES_PERIOD, 0, myLocListener);
		mLocationM.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, LOCATION_UPDATES_PERIOD, 0, myLocListener);
		lastKnownPosition = mLocationM.getLastKnownLocation(LocationManager.GPS_PROVIDER);
		//per sicurezza
		if (lastKnownPosition == null)
			lastKnownPosition = mLocationM.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
		
		//connectivity manager
		mConnManager = (ConnectivityManager)this.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetwork = mConnManager.getActiveNetworkInfo();
        
        //inizializzo lo stato
        if(activeNetwork != null && activeNetwork.isConnectedOrConnecting())
        	STATUS = STATUS_CONNECTED;
        else
        	STATUS = STATUS_DISCONNECTED;
        
        //stato dei metadati TODO controlla se sono gia' presenti
        METADATA_STATUS = STATUS_METADATA_ABSENT;
        
		//Geocoder per gestire i record che non hanno lat, lon ma solo indirizzo
		Place.setGeocoder(new Geocoder(this));
		
		//register receivers
		IntentFilter filter;
		filter = new IntentFilter(Variables.DOWNLOAD_FINISHED);
		registerReceiver(mBCReceiver, filter);
		filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
		registerReceiver(mBCReceiver, filter);
		filter = new IntentFilter(Variables.DOWNLOAD_METADATA_FINISHED);
		registerReceiver(mBCReceiver, filter);
	}
	
	@Override
	public void onDestroy() {
		super.onDestroy();
		mLocationM.removeUpdates(myLocListener);
		mLocationM.removeGpsStatusListener(mGPSStatusListener);
		unregisterReceiver(mBCReceiver);
	}
	
	@Override
	public IBinder onBind(Intent arg0) {
		return mBinder;
	}
	
	BroadcastReceiver mBCReceiver = new BroadcastReceiver(){

		@Override
		public void onReceive(Context context, Intent intent) {
			String action = intent.getAction();
			Log.v(MainActivity.TAG,action);
			if (action.equals(Variables.DOWNLOAD_FINISHED)) {
				//TODO
			}
			else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)){
				
				NetworkInfo activeNetwork = mConnManager.getActiveNetworkInfo();
				
				if(activeNetwork != null && activeNetwork.isConnectedOrConnecting()) {
					setNetworkStatus(ACTION_NETWORK_CONNECT);
					if(activeNetwork.getType() == ConnectivityManager.TYPE_WIFI)
						TYPE_NETWORK = TYPE_NETWORK_WIFI;
					else
						TYPE_NETWORK = TYPE_NETWORK_MOBILE;
					
					initializeMetaData();
				}
				else 
					if(intent.getExtras().getBoolean(ConnectivityManager.EXTRA_NO_CONNECTIVITY)){
						setNetworkStatus(ACTION_NETWORK_DISCONNECT);
						TYPE_NETWORK = TYPE_NETWORK_MOBILE;
						
						initializeMetaData();
				}
			}
			else if (action.equals(Variables.DOWNLOAD_METADATA_FINISHED)){
				if(intent.getExtras().getInt(Variables.DOWNLOAD_RESULT) == Variables.NO_ERROR)
					setMetaDataStatus(ACTION_METADATA_DOWNLOADED);
				else
					setMetaDataStatus(ACTION_METADATA_DOWNLOAD_ERROR);
				
				initializeMetaData();
			}
		}
		
	};
	
	void initializeMetaData(){
		//STATO
		switch(STATUS){
		case STATUS_DISCONNECTED:
			/*
			Intent i = new Intent(Settings.ACTION_WIRELESS_SETTINGS);
			i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
			startActivity(i);
			Utils.showToast(this, "Please enable network connection");*/
			break;
		case STATUS_CONNECTED:
			if(getMetaDataStatus() == STATUS_METADATA_DOWNLOADED)
				STATUS = STATUS_METADATA_READY;
			else
				break;
		case STATUS_METADATA_READY:
			break;
		}
		Log.v(MainActivity.TAG, "Current status :"+STATUS);
	}
	
	private void setNetworkStatus(String action){
		if(action != null){
			if(action.equals(ACTION_NETWORK_CONNECT)){
				STATUS = STATUS_CONNECTED;
				firstAttemptFailed = false;
			}
				
			if(action.equals(ACTION_NETWORK_DISCONNECT))
				STATUS = STATUS_DISCONNECTED;
		}
	}
	
	private int getMetaDataStatus(){
		if (METADATA_STATUS == STATUS_METADATA_ABSENT && !firstAttemptFailed){
			if(mActivity != null){
				DownloadThread t = new DownloadThread(mActivity);
				t.execute();
				METADATA_STATUS = STATUS_METADATA_RETRIEVING;
			}
		}
		if (METADATA_STATUS == STATUS_METADATA_RETRIEVING);
			
		if (METADATA_STATUS == STATUS_METADATA_DOWNLOADED);
			
		return METADATA_STATUS;
	}
	
	private void setMetaDataStatus(String action){
		if(action != null){
			if(action.equals(ACTION_METADATA_DOWNLOADED))
				METADATA_STATUS = STATUS_METADATA_DOWNLOADED;
			if(action.equals(ACTION_METADATA_DOWNLOAD_ERROR)){
				firstAttemptFailed = true;
				METADATA_STATUS = STATUS_METADATA_ABSENT;
			}
		}
	}
	
	private boolean isMetaDataReady() {
		return STATUS == STATUS_METADATA_READY;
	}
	
	// GPS objects
	
	private LocationListener myLocListener = new LocationListener(){  
		   
	    public void onLocationChanged(Location loc) {  
	        Log.v("newLocation", "new location: "+loc.getProvider()+","+loc.getLatitude()+","+loc.getLongitude());
	    	if(isBetterLocation(loc)){
	        	lastKnownPosition = loc;
	        	Log.v("newLocation","nuova loc accettata");
	    	}
	    	else
	    		Log.v("newLocation","nuova loc scartata");
	    }
	    
	    /**
	     * @param la Location appena acquisita
	     * @return true se la location appena ricevuta e' migliore della lastKnownPosition
	     */
	    private boolean isBetterLocation(Location loc){
	    	//avere una location e' sempre meglio che non averla
	    	if (lastKnownPosition == null)
	        	return true;
	    	
	    	//una location GPS e' sempre preferibile
	    	if (loc.getProvider().equals(LocationManager.GPS_PROVIDER))
	        	return true;
	    	
	    	//se invece viene dal network:
	    	
	    	long deltaTimeThreshold;
	    	float deltaAccuracyThreshold;
	    	switch(TYPE_NETWORK){
	    	case TYPE_NETWORK_WIFI:
	    		deltaTimeThreshold = DELTA_TIME_THRESHOLD_WIFI;
	    		deltaAccuracyThreshold = DELTA_ACC_THRESHOLD_WIFI;
	    		Log.v("newLocation", "wifi");
	    		break;
	    	case TYPE_NETWORK_MOBILE:
	    	default:
	    		deltaTimeThreshold = DELTA_TIME_THRESHOLD_MOBILE;
	    		deltaAccuracyThreshold = DELTA_ACC_THRESHOLD_MOBILE;
	    		Log.v("newLocation", "mobile");
	    		break;
	    	}
	    	
	    	long deltaTime = loc.getTime() - lastKnownPosition.getTime();
	    	Log.v("newLocation", "deltaTime: "+Long.toString(deltaTime));
	    	//se la nuova location e' molto piu' recente della vecchia, la prendo per buona
	    	if (deltaTime > deltaTimeThreshold - 500) //incertezza di 0.5s nel ricevere i location updates
	    		return true;
	    	
	    	float deltaAccuracy = loc.getAccuracy() - lastKnownPosition.getAccuracy();
	    	Log.v("newLocation", "newAcc: "+Float.toString(loc.getAccuracy()));
	    	Log.v("newLocation", "deltaAcc: "+Float.toString(deltaAccuracy));
	    	//un delta molto negativo indica che la accuracy e' migliorata
	    	if (deltaAccuracy < - deltaAccuracyThreshold)
	    		return true;
	    	
	    	//arrivati qui, vuol dire che la location viene dal network, non e' molto piu'
	    	//recente dell'ultima e non e' molto piu' precisa dell'ultima: la scarto
	    	return false;
	    }
	    
	    private boolean areLocationsEqual(Location l1, Location l2){
	    	if (l1 == null || l2 == null)
	    		return false;
	    	if (l1.getLatitude() == l2.getLatitude() &&
	    			l1.getLongitude() == l2.getLongitude() /*&&
	    			l1.getSpeed() == l2.getSpeed() &&
	    			l1.getBearing() == l2.getBearing()*/)
	    		return true;
	    	else
	    		return false;
	    }
	    
	    public void onProviderDisabled(String provider) {  
	        //Log.d(TAG, "onProviderDisabled");
	    }  
	  
	    public void onProviderEnabled(String provider) {  
	        //Log.d(TAG, "onProviderEnabled");  
	    }  
	  
	    public void onStatusChanged(String provider, int status, Bundle extras) {  
	    }  
	};
	
	private Listener mGPSStatusListener = new Listener() {
		
		@Override
		public void onGpsStatusChanged(int status) {
			switch (status) {
		        case GpsStatus.GPS_EVENT_FIRST_FIX:  
		            //Log.d(TAG, "onGpsStatusChanged First Fix");  
		            break;  
		        case GpsStatus.GPS_EVENT_SATELLITE_STATUS:  
		        	//Log.d(TAG, "onGpsStatusChanged Satellite");
		        	/*if (currentLocation != null){
		        		long delta = System.currentTimeMillis() - currentLocation.getTime();
	        			isGpsFix =  delta < GPS_FIX_THRESHOLD;
	        			if (!isGpsFix)
	        				fixWarn = "gps fix is lost "+delta;
	        		}*/
		        	break;  
		        case GpsStatus.GPS_EVENT_STARTED:  
		            //Log.d(TAG, "onGpsStatusChanged Started");  
		            break;  
		        case GpsStatus.GPS_EVENT_STOPPED:  
		            //Log.d(TAG, "onGpsStatusChanged Stopped");  
			}	
		}
	};
	
	/**
	 * I link ai metadati sono contenuti nel file metadata_links.xml
	 * Nel file places_array sono contenute le stringhe associate a ciascuno dei metadati,
	 * in modo che si possano impostare nel tab Settings le preferenze.
	 * Questa funzione ritorna un valore booleano per ogni metadato, in modo da sapere
	 * se bisogna includerlo nella ricerca o no.
	 * @return il vettore di booleani corrispondenti alle preferenze salvate
	 */
	Boolean[] getSettingsArray(){
		SharedPreferences prefs = getSharedPreferences(MY_PREFS, MODE_PRIVATE);
		String[] names = getResources().getStringArray(R.array.places);
		Boolean[] settings = new Boolean[names.length];
		for(int i = 0; i < names.length; i++){
			settings[i] = prefs.getBoolean(names[i], false);
		}
		return settings;
	}
	
	/**
	 * 
	 * @return il raggio di ricerca, in metri
	 */
	long getRange(){
		SharedPreferences prefs = getSharedPreferences(MY_PREFS, MODE_PRIVATE);
		int index = prefs.getInt(SettingsFragment.RANGE, 0);
		String[] values = getResources().getStringArray(R.array.spinner_array);
		return Long.valueOf(values[index]) * 1000L;
	}
	
	/**
	 * il binder implementa l'interfaccia che viene vista dall'Activity
	 * deve contenere metodi utili alla Activity
	 * @author Simone Panza
	 *
	 */
	public class ControllerBinder extends Binder implements ControllerServiceInterface{

		@Override
		public void getNearPlaces() {
			// TODO Auto-generated method stub
			if(isMetaDataReady()){
				DownloadData dd = new DownloadData(Controller.this, mActivity, c);
				dd.setDistance(getRange());
				dd.setCurrentLocation(getCurrentLocation());
				dd.execute(getSettingsArray());
			}
			else
				Log.v(MainActivity.TAG, "metadata non ancora pronti!");
			/*ArrayList<String> mList = new ArrayList<String>();
			mList.add("seriate");
			mList.add("pedrengo");
			*/
			//return (ArrayList<Place>) Place.lista;
		}

		@Override
		public void searchForPlaces(String searchString) {
			if(isMetaDataReady()){
				SearchData dd = new SearchData(Controller.this, mActivity, c,searchString);
				dd.execute(getSettingsArray());
			}
			else
				Log.v(MainActivity.TAG, "metadata non ancora pronti!");
		}
		
		@Override
		public Location getCurrentLocation() {
			
			/*if (lastKnownPosition != null)
				return lastKnownPosition;
			else
				return mLocationM.getLastKnownLocation(LocationManager.GPS_PROVIDER);*/
			return lastKnownPosition;	
			
			// in fase di testing usiamo una location nota
			/*Location loc = new Location(LocationManager.GPS_PROVIDER);
			loc.setLatitude(45.684297);
			loc.setLongitude(9.721599);
			//loc.setBearing(Float.valueOf(st.nextToken()));
			//loc.setSpeed(Float.valueOf(st.nextToken())/3.6f);
			loc.setTime(System.currentTimeMillis());
			return loc;*/
		}

		@Override
		public void setActivity(Activity activity) {
			mActivity = activity;
			initializeMetaData();
		}
		
	}
	public static float distanceTo(Place l){
		Location loc;
		if (lastKnownPosition != null){
			loc= lastKnownPosition;
			Log.d("latKnowPosition",Double.toString(lastKnownPosition.getLatitude()));
		}
		else{
			// in fase di testing usiamo una location nota
			loc = new Location(LocationManager.GPS_PROVIDER);
			loc.setLatitude(45.684297);
			loc.setLongitude(9.721599);
		}
		
		Location temp=new Location(LocationManager.GPS_PROVIDER);
		temp.setLatitude(l.getLatitude());
		temp.setLongitude(l.getLongitude());
		
		Float dis=loc.distanceTo(temp);
		Log.d("distance",Float.toString(dis));
		//Integer disInt=dis.intValue();
		
		return dis;	
	}
	
	
}
